/*
 * Project it.battlehorse.rcp.tools.dbbase
 * Created on Nov 24, 2005
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 * 
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 *
 * A copy of the LGPL is available also at
 * http://www.gnu.org/copyleft/lesser.html
 * 
 */ 
package it.battlehorse.rcp.tools.dbbase.internal;

import it.battlehorse.rcp.sl.annot.Injected;
import it.battlehorse.rcp.sl.annot.Serviceable;
import it.battlehorse.rcp.tools.dbbase.DatasourceConfig;
import it.battlehorse.rcp.tools.dbbase.DbBaseActivator;
import it.battlehorse.rcp.tools.dbbase.DbConfig;
import it.battlehorse.rcp.tools.dbbase.IDbManager;
import it.battlehorse.rcp.tools.dbbase.widgets.DataSourceContentProvider;
import it.battlehorse.rcp.tools.dbbase.widgets.DatasourceLabelProvider;

import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.sql.DataSource;

import org.apache.commons.dbcp.BasicDataSource;
import org.apache.commons.logging.Log;
import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.jface.viewers.ContentViewer;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.Viewer;

/**
 * Manages all the datasources and the active connections. Provides an implementation of the 
 * {@link IDbManager } interface. The provided implementation adopts the singleton pattern, so that
 * a single database manager is active in the application. 
 * 
 * @author battlehorse
 * @since Nov 24, 2005
 */
@Serviceable
public class DbManager implements IDbManager {
	
	/* A logger */
	private Log log;
	
	/* a helper class which handles store/load operations */
	private DbBaseStoreHelper helper;
	
	/**
	 * Injects a reference to a valid logger
	 * @param log
	 */
	@Injected
	public void setLog(Log log) {
		this.log = log;
	}
		
	
	/**
	 * The shared instance
	 */
	private static DbManager instance;
	
	/**
	 * Returns the shared instance of the class. This method should not be invoked
	 * directly : the db manager should be accessed using the service locator framework.
	 * 
	 * @return the shared db manager instance
	 */
	public static synchronized DbManager getInstance() {
		if (instance == null)
			instance =  new DbManager();
		
		return instance;
	}
	
	/**
	 * The list of valid databases (i.e. : databases for which an associated driver has
	 * been found).
	 */
	private List<DbConfig> dbConfigList;
	
	/**
	 * The list of viewers which are showing the available datasources and 
	 * need to be notified whenever they change.
	 */
	private List<Viewer> datasourceViewers;
	
	/**
	 * Contains the list of the current available datasources
	 */
	private List<DatasourceConfig> currentSources;
	
	/**
	 * Associates datasource configurations with datasource instances
	 */
	private Map<DatasourceConfig,BasicDataSource> datasources;	
	
	/**
	 * Defines if the available datasources has already been loaded or not
	 */
	private boolean loadedDatasources;
	
	/**
	 * Creates a new instance of the class
	 *
	 */
	private DbManager() {
		helper = new DbBaseStoreHelper(this);
		dbConfigList = helper.loadDbConfig();
		datasources = new HashMap<DatasourceConfig,BasicDataSource>();
		datasourceViewers = new ArrayList<Viewer>();				
		currentSources = new ArrayList<DatasourceConfig>();
		loadedDatasources =false;
	}
	
	/* (non-Javadoc)
	 * @see it.battlehorse.rcp.tools.dbbase.IDbManager#getDbConfigList()
	 */
	public List<DbConfig> getDbConfigList() {
		return dbConfigList;
	}
	
	/* (non-Javadoc)
	 * @see it.battlehorse.rcp.tools.dbbase.IDbManager#findDbConfig(java.lang.String)
	 */
	public DbConfig findDbConfig(String name) {
		if (dbConfigList != null) {
			for (DbConfig cfg: dbConfigList) {
				if (cfg.getName().equals(name))
					return cfg;
			}
		}
		return null;
	}	
	
	/* (non-Javadoc)
	 * @see it.battlehorse.rcp.tools.dbbase.IDbManager#findDatasourceConfig(java.lang.String)
	 */
	public DatasourceConfig findDatasourceConfig(String name) {
		if (!loadedDatasources)
			currentSources = loadDatasourceConfig(DbBaseActivator.getDefault().getPreferenceStore());
		
		for (DatasourceConfig cfg: currentSources) {
			if (cfg.getName().equals(name))
				return cfg;
		}
		return null;
	}
	
	/* (non-Javadoc)
	 * @see it.battlehorse.rcp.tools.dbbase.IDbManager#isIdle()
	 */
	public boolean isIdle() {
		int curActive = 0;
		for ( BasicDataSource ds : datasources.values()) {
			curActive += ds.getNumActive();
		}
		
		return curActive == 0;
	}

	/* (non-Javadoc)
	 * @see it.battlehorse.rcp.tools.dbbase.IDbManager#loadDatasourceConfig(org.eclipse.jface.preference.IPreferenceStore)
	 */	
	public List<DatasourceConfig> loadDatasourceConfig(IPreferenceStore store) {
		loadedDatasources =true;
		return helper.loadDatasourceConfig(store);
	}
	
	/* (non-Javadoc)
	 * @see it.battlehorse.rcp.tools.dbbase.IDbManager#storeDatasourceConfig(org.eclipse.jface.preference.IPreferenceStore, java.util.List)
	 */
	public void storeDatasourceConfig(IPreferenceStore store,List<DatasourceConfig> l) {
		
		// close all the datasources
		shutdown();
		
		// store the configuration. The new sources will be created on demand.
		helper.storeDatasourceConfig(store, l);
		currentSources = l;
		notifyDatasourceViewers(currentSources);
	}
	
	/* (non-Javadoc)
	 * @see it.battlehorse.rcp.tools.dbbase.IDbManager#getDataSource(it.battlehorse.rcp.tools.dbbase.DatasourceConfig)
	 */
	public DataSource getDataSource(DatasourceConfig ds) {
		return getDataSource(ds,true);
	}
	
	/* (non-Javadoc)
	 * @see it.battlehorse.rcp.tools.dbbase.IDbManager#getDataSource(it.battlehorse.rcp.tools.dbbase.DatasourceConfig, boolean)
	 */
	public DataSource getDataSource(DatasourceConfig ds, boolean storeConfiguration) {
		DataSource source = datasources.get(ds);
		if (source == null) {
			
			// First-time datasource configuration
			source = setUpDataSource(ds, storeConfiguration);
		}
		
		if (log.isTraceEnabled())
			log.trace("Offering datasource " + ds.getName());
		return source;		
	}
	
	/**
	 * Add a viewer to the list of viewers which need to be notified whenever
	 * the datasources list changes. As soon as the viewer is added, it's {@code input}
	 * property is set with the up-to-date list of datasources. If the viewer is an
	 * instance of {@code ContentViewer}, then a {@link DataSourceContentProvider} and a
	 * {@link DatasourceLabelProvider} are set on the viewer.
	 * 
	 * @param v the viewer
	 */
	public void addDatasourceViewer(Viewer v) {
		datasourceViewers.add(v);
		if (!loadedDatasources)
			currentSources = loadDatasourceConfig(DbBaseActivator.getDefault().getPreferenceStore());
		
		if (v instanceof ContentViewer) {
			ContentViewer cv = (ContentViewer) v;
			cv.setLabelProvider(new DatasourceLabelProvider());
			cv.setContentProvider(new DataSourceContentProvider());
		}
		v.setInput(currentSources);
	}
	
	/* (non-Javadoc)
	 * @see it.battlehorse.rcp.tools.dbbase.IDbManager#removeDatasourceViewer(org.eclipse.jface.viewers.Viewer)
	 */
	public void removeDatasourceViewer(Viewer v) {
		datasourceViewers.remove(v);
	}
	
	/* (non-Javadoc)
	 * @see it.battlehorse.rcp.tools.dbbase.IDbManager#shutdown()
	 */
	public void shutdown() {
		for ( DatasourceConfig ds : datasources.keySet()) {
			try {
				datasources.get(ds).close();
			}
			catch(SQLException sqle) {
				log.error(String.format("Unable to shutdown datasource %s : %s", ds.getName() , sqle.getMessage()),sqle);
			}
		}
		
		datasources.clear();
		
			
			
	}

	/**
	 * Performs the needed setup operations to instantiate a datasource 
	 * 
	 * @param ds the datasource configuration
	 * @param storeConfiguration defines if the returned datasource has to be stored into
	 * 		the internal registry.
	 * @return the datasource
	 */
	private DataSource setUpDataSource(DatasourceConfig ds,boolean storeConfiguration) {
				
		if (log.isTraceEnabled())
			log.trace("Creating datasource " + ds.getName());
	
		BasicDataSource bds = new BasicDataSource();
		bds.setDriverClassName(ds.getDatabase().getDriverClassName());
		bds.setUsername(ds.getUser());
		bds.setPassword(ds.getPassword());
		bds.setUrl(ds.getConnectionUrl());
		bds.setMaxActive(0);		// unlimited pool
		bds.setInitialSize(0);		// empty pool on init
		bds.setMinIdle(0);			// clean the pool if no-one is using it
		bds.setMaxIdle(1);			// keep at most a spare idle connection
		
		if (storeConfiguration)
			datasources.put(ds,bds);
		
		return bds;
	}
	
	/**
	 * Notifies all the registered datasource viewers that the list of datasources
	 * has changed. Note that this method is invoked only when the datasources are idle,
	 * so we're not changing the viewers when they are in use.
	 * <p>
	 * The method tries to preserve the current selection on every viewer (unless the selected
	 * datasource is removed from the list of available sources).
	 *  
	 * @param newSources the up-to-date list of datasources.
	 */
	private void notifyDatasourceViewers(List<DatasourceConfig> newSources) {
		for (Viewer v : datasourceViewers) {
			
			// extract and save previous selection
			ISelection prevSelection = v.getSelection();
			DatasourceConfig selectedConfig = null;			
			if (prevSelection != null && prevSelection instanceof IStructuredSelection) {
				IStructuredSelection structSel = (IStructuredSelection) prevSelection;
				Object selectedItem = structSel.getFirstElement();
				if (selectedItem != null && newSources.contains(selectedItem)) {
					selectedConfig = (DatasourceConfig) selectedItem;					
				}
			}
			
			// change the input
			v.setInput(newSources);
			
			// restore selection
			if (prevSelection != null && selectedConfig != null)
				v.setSelection(prevSelection);
			
			// refresh the viewer
			v.refresh();
		}
	}		
	
}
